--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 76e5504698ae036c0705e1e6c9382516a4c0e8ca
Parents : 48cf573
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-05T00:20:35-05:00
chore(ci): normalize pycodec2 dylib layout for macOS to ensure consistent architecture handling in unify-backend script
Changes
4 files changed, 110 insertions(+), 6 deletions(-)
Diff
diff --git a/scripts/ci/github-install-deps.sh b/scripts/ci/github-install-deps.sh
index 5ae59d97..051d76e8 100755
--- a/scripts/ci/github-install-deps.sh
+++ b/scripts/ci/github-install-deps.sh
@@ -38,6 +38,12 @@ import numpy
from numpy._core._multiarray_umath import _add_newdoc_ufunc
print('arm64 venv numpy', numpy.__version__, 'ok')
"
+ # The published macOS wheel bundles libcodec2 under pycodec2/.dylibs/. Flatten
+ # it to pycodec2/libcodec2.dylib so this tree's relative layout matches the
+ # darwin-x64 slice built in scripts/ci/github-install-macos-x64-python-deps.sh,
+ # which scripts/unify-backend-plain-files.sh requires to merge the two trees.
+ bash "$(dirname "$0")/macos-normalize-pycodec2-dylib.sh" "${ROOT}/.venv/bin/python" ||
+ echo "github-install-deps: pycodec2 dylib normalization failed, continuing (unify-backend may drop it later)" >&2
fi
if [[ "$(uname -s)" == "Darwin" ]]; then
diff --git a/scripts/ci/github-install-macos-x64-python-deps.sh b/scripts/ci/github-install-macos-x64-python-deps.sh
index e5e98581..b365c126 100755
--- a/scripts/ci/github-install-macos-x64-python-deps.sh
+++ b/scripts/ci/github-install-macos-x64-python-deps.sh
@@ -128,18 +128,23 @@ uv pip install --python "$_PY" \
--python-platform x86_64-apple-darwin \
"$_pycodec2_wheel"
+# pycodec2's setup.py links with -lcodec2, so the extension records Homebrew's
+# absolute install name for libcodec2 (e.g. /usr/local/opt/codec2/lib/libcodec2.1.2.dylib).
+# That only resolves on this CI runner. Bundle the dylib next to the extension and
+# rewrite the load command to @loader_path/libcodec2.dylib so it is self-contained,
+# and matches the relative layout scripts/unify-backend-plain-files.sh expects when
+# reconciling this slice against the arm64 wheel's .dylibs/ bundle.
if [[ -n "${_codec2:-}" ]]; then
_pycodec2_dir="$(arch -x86_64 "$_PY" -c 'import pathlib, pycodec2; print(pathlib.Path(pycodec2.__file__).resolve().parent)')"
for _lib in "${_codec2}/lib/libcodec2.dylib" "${_codec2}/lib/libcodec2.so"; do
if [[ -f "$_lib" ]]; then
- cp -f "$_lib" "${_pycodec2_dir}/libcodec2.so"
- if [[ "$_lib" == *.dylib ]]; then
- cp -f "$_lib" "${_pycodec2_dir}/libcodec2.dylib"
- fi
+ cp -f "$_lib" "${_pycodec2_dir}/libcodec2.dylib"
break
fi
done
fi
+arch -x86_64 bash "$(dirname "$0")/macos-normalize-pycodec2-dylib.sh" "$_PY" ||
+ echo "github-install-macos-x64-python-deps: pycodec2 dylib normalization failed, continuing (unify-backend may drop it later)" >&2
arch -x86_64 "$_PY" scripts/patch_lxst_pyogg_ogg_ctypes.py
diff --git a/scripts/ci/macos-normalize-pycodec2-dylib.sh b/scripts/ci/macos-normalize-pycodec2-dylib.sh
new file mode 100755
index 00000000..d9b9eda3
--- /dev/null
+++ b/scripts/ci/macos-normalize-pycodec2-dylib.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+# Normalize the installed pycodec2 package's bundled libcodec2 into a fixed,
+# arch-independent relative layout:
+#
+# pycodec2/pycodec2*.so (extension module; @loader_path/libcodec2.dylib)
+# pycodec2/libcodec2.dylib (single canonical name, no .dylibs/ subfolder)
+#
+# Published macOS wheels bundle libcodec2 under pycodec2/.dylibs/libcodec2.<ver>.dylib
+# (delocate convention). Building pycodec2 from sdist for architectures without a
+# published wheel (e.g. cp314 macOS x86_64) instead links against whatever Homebrew
+# codec2 path was on LDFLAGS at build time. Without normalization the two cx_Freeze
+# build trees (darwin-arm64 vs darwin-x64) have different relative file layouts for
+# the same logical dependency, so scripts/unify-backend-plain-files.sh treats the
+# bundled dylib as arch-only and drops it from BOTH trees, breaking Codec2 audio
+# (voice messages) in the shipped universal macOS app.
+#
+# Usage: macos-normalize-pycodec2-dylib.sh <python-executable>
+set -euo pipefail
+
+PY="${1:?usage: macos-normalize-pycodec2-dylib.sh <python-executable>}"
+
+if [[ "$(uname -s)" != "Darwin" ]]; then
+ echo "macos-normalize-pycodec2-dylib: skipping (not macOS)" >&2
+ exit 0
+fi
+
+_site_dir="$("$PY" -c 'import pycodec2, pathlib; print(pathlib.Path(pycodec2.__file__).resolve().parent)' 2>/dev/null || true)"
+if [[ -z "$_site_dir" || ! -d "$_site_dir" ]]; then
+ echo "macos-normalize-pycodec2-dylib: pycodec2 not installed, skipping" >&2
+ exit 0
+fi
+
+_ext_so="$(find "$_site_dir" -maxdepth 1 -name 'pycodec2*.so' -print -quit)"
+if [[ -z "$_ext_so" ]]; then
+ echo "macos-normalize-pycodec2-dylib: no pycodec2 extension module found under ${_site_dir}, skipping" >&2
+ exit 0
+fi
+
+_target="${_site_dir}/libcodec2.dylib"
+_dylibs_dir="${_site_dir}/.dylibs"
+_src_dylib=""
+
+if [[ -d "$_dylibs_dir" ]]; then
+ _src_dylib="$(find "$_dylibs_dir" -maxdepth 1 -name 'libcodec2*.dylib' -print -quit)"
+fi
+if [[ -z "$_src_dylib" ]]; then
+ for _candidate in "${_site_dir}/libcodec2.dylib" "${_site_dir}/libcodec2.so"; do
+ if [[ -f "$_candidate" ]]; then
+ _src_dylib="$_candidate"
+ break
+ fi
+ done
+fi
+
+if [[ -z "$_src_dylib" ]]; then
+ echo "macos-normalize-pycodec2-dylib: no bundled libcodec2 found under ${_site_dir}, skipping" >&2
+ exit 0
+fi
+
+if [[ "$_src_dylib" != "$_target" ]]; then
+ cp -f "$_src_dylib" "$_target"
+fi
+if [[ -d "$_dylibs_dir" ]]; then
+ rm -rf "$_dylibs_dir"
+fi
+if [[ -f "${_site_dir}/libcodec2.so" && "${_site_dir}/libcodec2.so" != "$_target" ]]; then
+ rm -f "${_site_dir}/libcodec2.so"
+fi
+
+_old_ref="$( (otool -L "$_ext_so" | awk '/libcodec2/{print $1; exit}') 2>/dev/null || true)"
+if [[ -n "$_old_ref" && "$_old_ref" != "@loader_path/libcodec2.dylib" ]]; then
+ install_name_tool -change "$_old_ref" "@loader_path/libcodec2.dylib" "$_ext_so"
+ codesign --force --sign - "$_ext_so" >/dev/null 2>&1 || true
+ echo "macos-normalize-pycodec2-dylib: rewrote ${_old_ref} -> @loader_path/libcodec2.dylib in $(basename "$_ext_so")"
+fi
+
+echo "macos-normalize-pycodec2-dylib: normalized $(basename "$_ext_so") + libcodec2.dylib under ${_site_dir}"
diff --git a/scripts/unify-backend-plain-files.sh b/scripts/unify-backend-plain-files.sh
index 56b9ae07..52192d76 100755
--- a/scripts/unify-backend-plain-files.sh
+++ b/scripts/unify-backend-plain-files.sh
@@ -36,8 +36,11 @@ copy_missing() {
if [[ "$ft" == Mach-O* ]]; then
echo "unify-backend: dropping arch-only Mach-O for consistency: $rel" >&2
echo " ($label); source reports: $ft" >&2
- echo " Hint: ensure libyaml is available for x86_64 (arch -x86_64 brew install libyaml)" >&2
- echo " so PyYAML's C extension compiles for darwin-x64 and both trees match." >&2
+ echo " Hint: this native library/extension only exists in one arch's" >&2
+ echo " cx_Freeze output. Check the corresponding per-OS dependency" >&2
+ echo " install script builds/bundles it for both darwin-arm64 and" >&2
+ echo " darwin-x64 (e.g. missing a Homebrew lib on one arch, or a" >&2
+ echo " package that only ships a wheel for one arch)." >&2
rm -f "$src_file"
dropped=$((dropped + 1))
continue
@@ -66,6 +69,19 @@ while IFS= read -r -d '' rel; do
filetype=$(file --brief --no-pad "$arm64_file" 2>/dev/null || true)
if [[ "$filetype" != Mach-O* ]]; then
+ # .pyc is always portable CPython bytecode regardless of what else
+ # lives in the same package directory: differences here come from
+ # the embedded source mtime/size (PEP 552 header) or from hash-seed-
+ # dependent constant ordering (e.g. frozenset literals), never from
+ # the target architecture. Unlike a real native-extension conflict,
+ # unifying it can't break the .so/.dylib siblings, which are handled
+ # entirely separately below.
+ if [[ "$rel" == *.pyc ]]; then
+ cp "$arm64_file" "$x64_file"
+ echo " unified (.pyc, source-independent of arch): $rel"
+ unified=$((unified + 1))
+ continue
+ fi
pkg_dir="$(dirname "$rel")"
if find "$ARM64_DIR/$pkg_dir" "$X64_DIR/$pkg_dir" -maxdepth 1 \
\( -name '*.so' -o -name '*.dylib' -o -name '*.bundle' \) -print -quit 2>/dev/null | grep -q .; then
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────